// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Jeff Stewart
//
// Implements a probe to exercise SMTP MSA over TLS or SSL on port 587 or 465.

#include <QtNetwork/QHostInfo>
#include <QtCore/QDir>
#include <QtCore/QMap>
#include <QtCore/QRegExp>
#include <QtCore/QString>
#include <QtCore/QTime>
#include <QtNetwork/QtNetwork>
#include <algorithm>

#include "probes/smtpprobe.h"
#include "probes/ssl.h"

namespace popdiag {

typedef enum SmtpStatusCodes {
  SMTP_BANNER_OK = 220,
  SMTP_QUIT_OK = 221,
  SMTP_GENERIC_OK = 250,
  SMTP_OK_TO_SEND_DATA = 354,
  SMTP_BANNER_LOAD_SHED = 421,
};

SmtpProbeFactory::SmtpProbeFactory(const QString &username,
                                   const QString &password,
                                   const QString &smtp_server_name,
                                   int smtp_port)
    : username_(username),
      password_(password),
      smtp_server_name_(smtp_server_name),
      smtp_port_(smtp_port) {
}

Probe *SmtpProbeFactory::CreateProbe(const QHostAddress &addr,
                                     bool do_complete_probe) {
  return new SmtpProbe(username_, password_, smtp_server_name_,
                       addr, smtp_port_, !do_complete_probe);
}
const int SmtpProbe::kTimeout = 30000;

SmtpProbe::SmtpProbe(const QString &username,
                     const QString &password,
                     const QString &smtp_server,
                     const QHostAddress &host_address,
                     int smtp_port,
                     bool probe_ehlo_only)
    : Probe("Smtp"), username_(username), password_(password),
      smtp_server_(smtp_server), smtp_port_(smtp_port),
      base_socket_(NULL), src_(NULL), state_(INIT),
      max_state_(probe_ehlo_only ? AUTH : DISCONNECTED),
      host_address_(host_address),
      connect_timer_(NULL),
      ssl_negotiation_timer_(NULL), fault_(OK),
      probe_ehlo_only_(probe_ehlo_only), we_closed_(false) {
}

SmtpProbe::~SmtpProbe() {
}

void SmtpProbe::GetProgress(int *completed, int *estimated_total) {
  *completed = state_;
  *estimated_total = max_state_;
}

void SmtpProbe::SetState(TestState t) {
  state_ = t;
  emit ProgressMade(t, max_state_);
}

QString SmtpProbe::GetDisplayText() {
  switch (fault_) {
    case OK:
      return tr("The SMTP server appears to be working properly "
                "for your account.",
                "The tests uncovered no problems using the SMTP server.");
    case RESOLVE_FAILED:
      return tr("The SMTP server does not resolve in DNS. "
                "Is the network connected?",
                "We could not map the SMTP server hostname to an IP address "
                "using the Domain Name System (DNS). This is often a sign "
                "that the computer is not connected to the internet.");
    case CONNECTION_REFUSED:
      return tr("The SMTP server refused our connection attempt.",
                "Establishing a TCP connection failed because the "
                "remote server refused to accept the connection.");
    case SERVER_CLOSED_PREMATURELY:
      return tr("The SMTP server closed the connection prematurely",
                "Some time during the SMTP protocol conversation, the server "
                "closed the TCP connection unexpectedly.");
    case SOCKET_ERROR:
      return tr("We were unable to use the network to "
                "connect to the SMTP server.",
                "Using the local Sockets API, we were unable to get a socket "
                "to use to connect to the SMTP server.");
    case SMTP_SERVER_SHEDDING_LOAD:
      return tr("The SMTP server is overloaded and is "
                "turning away connections.",
                "We can't talk to the SMTP server because it is overloaded "
                "and turned us away.");
    case SMTP_BANNER_BAD_RESPCODE:
      return tr("The SMTP server's banner has the wrong status code.",
                "The SMTP server's first response line (the banner) "
                "had a status code we didn't expect.");
    case SMTP_SERVER_RESPONSE_MALFORMED:
      return tr("The SMTP server sent a malformed response to our command.",
                "The SMTP server responded to our command "
                "in an inproper way.");
    case SMTP_SERVER_RESPONSE_TIMED_OUT:
      return tr("The SMTP server did not respond to "
                "our command in a timely fashion.",
                "Reports a timeout in the SMTP conversation.");
    case SMTP_SERVER_BAD_RESPCODE:
      return tr("The SMTP server returned an unexpected response code.",
                "The SMTP server responded to our command with an "
                "inproper status code.");
    case SMTP_SERVER_NOT_OFFERING_TLS:
      return tr("The SMTP server does not offer to secure the connection."
                " It should.",
                "We require a secure connection. The SMTP server "
                "is not offering to secure the connection.");
    case SMTP_SERVER_NOT_OFFERING_AUTH:
      return tr("The SMTP server does not offer to accept logins. It should.",
                "We expect to have to login to the SMTP server. "
                "This server isn't asking for a login.");
    case SMTP_SERVER_CERT_SELFSIGNED:
      return tr("The SSL certificate for the SMTP server is not valid."
                " It is self-signed, or a certificate in the certifying "
                "authority chain is self-signed.",
                "The SSL certificate presented by the server is unacceptable.");
    case SMTP_SERVER_CERT_EXPIRED:
      return tr("The SSL certificate for the SMTP server is expired.",
                "The SSL certificate presented by the server is unacceptable.");
    case SMTP_SERVER_CERT_NOTYETVALID:
      return tr("The SSL certificate for the SMTP server is not yet valid.",
                "The SSL certificate presented by the server is unacceptable.");
    case SMTP_SERVER_CERT_CANNOTVALIDATE:
      return tr("We cannot validate the SSL certificate for the SMTP server.",
                "The SSL certificate presented by the server is unacceptable.");
    case SMTP_SERVER_CERT_NAME_MISMATCH:
      return tr("The SSL certificate for the SMTP server does "
                "not match the hostname we used to connect to it.",
                "The hostname presented in the SSL certificate is different "
                "than the hostname we used to connect to it.");
    case SMTP_SERVER_CAFILE_NOT_FOUND:
      // file of acceptable SSL certifying authorities was not found where
      // we expected it.  We cannot evaluate SSL certificate validity.
      return tr("There is a problem with the installation of "
                "this program (CAFile).",
                "A needed configuration file (CAFile) is missing from "
                "the installation.");
    case SMTP_SERVER_LOGIN_REJECTED:
      return tr("The SMTP server did not accept a login with the given "
                "username and password. This can represent a problem "
                "with your account, but it also can represent a problem "
                "with the system.",
                "Login failed");
    case SMTP_SERVER_LOGIN_BAD_CODE:
      return tr("The SMTP server responded in an unexpected way "
                "to our login attempt.",
                "The response code to our login attempt was unexpected.");
    case SMTP_SERVER_LOGIN_BACKEND_OFFLINE:
      return tr("The SMTP service is temporarily unavailable. "
                "Please wait a few minutes and try again.",
                "Informs the user that logins are failing "
                "but that they should work soon.");
    case SMTP_SERVER_REJECTED_MAIL_FROM:
      return tr("The SMTP server refused to accept mail from your account.",
                "The server refused our attempt to send mail "
                "from this account.");
    case SMTP_SERVER_REJECTED_RCPT_TO:
      return tr(
          "The SMTP server rejected mail sent to our diagnostic address.",
          "The server will not accept mail to our destination address.");
    case SMTP_SERVER_FAILED_RSET:
      return tr("The SMTP Server responded incorrectly to the RSET command.",
                "The server responded incorrectly to a request to "
                "reset its state.");
    case SMTP_SERVER_REJECTED_QUIT:
      return tr("The SMTP Server responded incorrectly to the QUIT command.",
                "We asked the server to shutdown this connection "
                "and somehow that failed.");
    case CONNECT_TIMED_OUT:
      return tr("Connecting to the SMTP Server timed out.",
                "Establishing a TCP connection to the SMTP server failed "
                "to complete in a timely manner.");
    case UNKNOWN:
      // fall through to default handling
      ;
  }
  CHECK(false);
  return tr("An unknown error occurred.", "This should NEVER happen.");
}

int SmtpProbe::GetFaultId() {
  return fault_;
}

void SmtpProbe::StartProbe() {
  ClearLog();
  StartLogDetails();
  StartProbeConnect();
}

void SmtpProbe::Cleanup() {
  we_closed_ = true;
  if (base_socket_.get() != NULL) {
    base_socket_->close();
  }
}

void SmtpProbe::ProbeDone(SmtpProbeFault fault_found) {
  // don't allow double faults.
  if (we_closed_ || fault_ != OK) {
    return;
  }
  Cleanup();
  EndLogDetails();
  fault_ = fault_found;
  emit ProbeComplete(fault_ == OK ? PROBE_OK : PROBE_FAILED);
}

void SmtpProbe::StartProbeConnect() {
  SetState(CONNECT);

  connect_timer_ = new QTimer(this);
  connect(connect_timer_, SIGNAL(timeout()),
          this, SLOT(OnConnectTimedOut()));
  connect_timer_->setSingleShot(true);
  connect_timer_->start(kTimeout);

  base_socket_.reset(new RenamableSslSocket());
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this, SLOT(OnConnectError(QAbstractSocket::SocketError))));
  CHECK(connect(base_socket_.get(), SIGNAL(connected()),
                this, SLOT(OnConnected())));
  CHECK(connect(base_socket_.get(), SIGNAL(disconnected()),
                this, SLOT(OnDisconnected())));
  src_ = base_socket_.get();
  LogDetails("AttemptConnect", true, QString("%1:%2")
             .arg(host_address_.toString())
             .arg(smtp_port_));
  base_socket_->connectToHost(host_address_, smtp_port_);
}

void SmtpProbe::OnConnected() {
  connect_timer_->stop();
  LogDetails("Connect", true, "");
  // socket is currently "named" by IP.  SSL cert validation
  // will fail with a name match.  That's why we used a Renamable
  // TCP Socket.  So replace the name out from under it with the
  // DNS name:
  base_socket_->SetDnsName(smtp_server_);
  if (smtp_port_ == SMTP_MSA_SSL_PORT) {
    StartProbeNegotiateTls();
  } else {
    StartProbeBanner();
  }
}

void SmtpProbe::OnDisconnected() {
  connect_timer_->stop();
  if (we_closed_) {
    return;
  }
  if (state_ == QUIT && base_socket_->bytesAvailable() == 0) {
    LogDetails("QUIT", false, "Server dropped the connection before responding "
                              "to the QUIT command.");
    ProbeDone(OK);
  } else {
    LogDetails(this->StepNameFromState(),
               false, "Server dropped the connection");
    ProbeDone(SERVER_CLOSED_PREMATURELY);
  }
}

void SmtpProbe::OnConnectTimedOut() {
  LogDetails("Connect", false, "TCP Connect timed out");
  ProbeDone(CONNECT_TIMED_OUT);
}

void SmtpProbe::OnConnectError(QAbstractSocket::SocketError err) {
  connect_timer_->stop();
  switch (err) {
    case QAbstractSocket::ConnectionRefusedError:
      LogDetails("Connect", false, "Connection refused");
      ProbeDone(CONNECTION_REFUSED);
    case QAbstractSocket::RemoteHostClosedError:
      Debug("SMTP remote host closed, %d bytes to read\n",
            base_socket_->bytesAvailable());
      if (state_ != QUIT || base_socket_->bytesAvailable() == 0) {
        // QAbstractSocket::SocketError is apparently sometimes
        // emitted even when there is still data available,
        // containing the valid QUIT response from the server.
        LogDetails("Connect", false,
                   "Server closed the connection before the banner");
        ProbeDone(SERVER_CLOSED_PREMATURELY);
      } else {
        break;
      }
    case QAbstractSocket::HostNotFoundError:
      LogDetails("Connect", false,
                  "Connect with IP parameter failed with a resolve error?");
      ProbeDone(RESOLVE_FAILED);
    case QAbstractSocket::SocketTimeoutError:
      LogDetails("Connect", false,
                  "Connect with server timed out");
      ProbeDone(CONNECT_TIMED_OUT);
    case QAbstractSocket::SocketAccessError:
    case QAbstractSocket::SocketResourceError:
    case QAbstractSocket::NetworkError:
    case QAbstractSocket::UnsupportedSocketOperationError:
    case QAbstractSocket::UnknownSocketError:
    default:
      QString error_string = "no socket";
      if (base_socket_ != NULL) {
        error_string = base_socket_->errorString();
      }
      LogDetails("Connect", false,
                  QString("Failed with code %1 (%2)")
                         .arg(err).arg(error_string));
      ProbeDone(SOCKET_ERROR);
  }
}

const char *SmtpProbe::StepNameFromState() {
  switch (state_) {
    case INIT:
      return "INIT";
    case RESOLVE:
      return "RESOLVE";
    case CONNECT:
      return "CONNECT";
    case BANNER:
      return "BANNER";
    case EHLO:
      return "EHLO";
    case STARTTLS:
      return "STARTTLS";
    case NEGOTIATESSL:
      return "NEGOTIATESSL";
    case REEHLO:
      return "REEHLO";
    case AUTH:
      return "AUTH";
    case MAILFROM:
      return "MAILFROM";
    case RCPTTO:
      return "RCPTTO";
    case RSET:
      return "RSET";
    case QUIT:
      return "QUIT";
    default:
      return "????";
  }
}

void SmtpProbe::FinishCommand(SmtpResponseReader *reader) {
  if (we_closed_) {
    return;
  }
  QString fault_name = "";
  SmtpProbeFault code = OK;
  switch (reader->response_type()) {
    case SMTP_RESPONSE_COMPLETE:
      ProcessSmtpResponse(reader);
      return;
    case SMTP_RESPONSE_MALFORMED:
      fault_name = "Malformed";
      code = SMTP_SERVER_RESPONSE_MALFORMED;
      break;
    case SMTP_SOCKET_DROPPED:
      fault_name = "Dropped";
      code = SERVER_CLOSED_PREMATURELY;
      break;
    case SMTP_RESPONSE_TIMEOUT:
      fault_name = "Timeout";
      code = SMTP_SERVER_RESPONSE_TIMED_OUT;
      break;
  }
  LogDetails(StepNameFromState(), false,
             fault_name + ": " + reader->describe());
  ProbeDone(code);
}

void SmtpProbe::ProcessSmtpResponse(SmtpResponseReader *reader) {
  CHECK_EQ(reader->response_type(), SMTP_RESPONSE_COMPLETE);
  switch (state_) {
    case BANNER:
      ProcessBannerResponse(reader);
      break;
    case EHLO:
      ProcessEhloResponse(reader);
      break;
    case STARTTLS:
      ProcessStartTlsResponse(reader);
      break;
    case REEHLO:
      ProcessEhloResponse(reader);
      break;
    case AUTH:
      ProcessAuthResponse(reader);
      break;
    case MAILFROM:
      ProcessMailFromResponse(reader);
      break;
    case RCPTTO:
      ProcessRcptToResponse(reader);
      break;
    case RSET:
      ProcessRsetResponse(reader);
      break;
    case QUIT:
      ProcessQuitResponse(reader);
      break;
    default:
      CHECK(false);
  }
}

SmtpResponseReader *SmtpProbe::GetReader() {
  SmtpResponseReader *reader = new SmtpResponseReader(this, src_, kTimeout);
  CHECK(connect(reader, SIGNAL(ResponseRead(SmtpResponseReader *)),
                this, SLOT(FinishCommand(SmtpResponseReader *))));
  return reader;
}

void SmtpProbe::StartProbeBanner() {
  SetState(BANNER);
  GetReader()->SendCommand("");
}

void SmtpProbe::ProcessBannerResponse(SmtpResponseReader *reader) {
  if (reader->response_code() == SMTP_BANNER_LOAD_SHED) {
    LogDetails("Banner", false, "Shedding Load: "  + reader->describe());
    ProbeDone(SMTP_SERVER_SHEDDING_LOAD);
    return;
  }
  if (reader->response_code() != SMTP_BANNER_OK) {
    LogDetails("Banner", false, "Wrong code: " + reader->describe());
    ProbeDone(SMTP_BANNER_BAD_RESPCODE);
    return;
  }
  LogDetails("Banner", true, reader->describe());

  StartProbeEhlo();
}

void SmtpProbe::StartProbeEhlo() {
  bool secured = base_socket_->isEncrypted();
  if (!secured) {
    SetState(EHLO);
  } else {
    SetState(REEHLO);
  }
  QString localhost = QHostInfo::localHostName();
  QString cmd = "EHLO " + localhost;
  GetReader()->SendCommand(cmd);
}

void SmtpProbe::ProcessEhloResponse(SmtpResponseReader *reader) {
  bool secured = base_socket_->isEncrypted();

  if (reader->response_code() != SMTP_GENERIC_OK) {
    LogDetails("EHLO", false, "Code Wrong: " + reader->describe());
    ProbeDone(SMTP_SERVER_BAD_RESPCODE);
    return;
  }

  bool auth_plain_seen = false;
  bool starttls_seen = false;
  for (QList<QString>::const_iterator i = reader->response_lines().begin();
       i != reader->response_lines().end(); i++) {
    QString upcap = i->toUpper();
    if (upcap == "STARTTLS") {
      starttls_seen = true;
    }
    if (upcap.startsWith("AUTH ") && upcap.indexOf("PLAIN") != -1) {
      auth_plain_seen = true;
    }
  }
  if (!secured) {
    if (!starttls_seen) {
      LogDetails("EHLO", false, "TLS Not offered");
      ProbeDone(SMTP_SERVER_NOT_OFFERING_TLS);
      return;
    }
  } else {
    if (!auth_plain_seen) {
      LogDetails("EHLO", false, "AUTH PLAIN not offered");
      ProbeDone(SMTP_SERVER_NOT_OFFERING_AUTH);
      return;
    }
  }
  LogDetails("EHLO", true, "");

  if (secured) {
    if (probe_ehlo_only_) {
      ProbeDone(OK);
      return;
    }
    StartProbeAuth();
  } else {
    StartProbeStartTls();
  }
}

void SmtpProbe::StartProbeStartTls() {
  SetState(STARTTLS);
  GetReader()->SendCommand("STARTTLS");
}

void SmtpProbe::ProcessStartTlsResponse(SmtpResponseReader *reader) {
  if (reader->response_code() != SMTP_BANNER_OK) {
    LogDetails("STARTTLSCMD", false, "Wrong Code: " + reader->describe());
    ProbeDone(SMTP_SERVER_BAD_RESPCODE);
    return;
  }
  LogDetails("STARTTLSCMD", true, reader->describe());
  StartProbeNegotiateTls();
}

void SmtpProbe::StartProbeNegotiateTls() {
  SetState(NEGOTIATESSL);
  QDir dir = QDir::current();
  if (!SetCACertificates(base_socket_.get())) {
    LogDetails("SSL", false, "Trusted CA file not found");
    ProbeDone(SMTP_SERVER_CAFILE_NOT_FOUND);
    return;
  }
  CHECK(connect(base_socket_.get(),
                SIGNAL(sslErrors(const QList<QSslError> &)),
                this,
                SLOT(OnSslError(const QList<QSslError> &))));
  CHECK(connect(base_socket_.get(), SIGNAL(encrypted()),
                this, SLOT(OnSslEstablished())));
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this,
                SLOT(OnError(QAbstractSocket::SocketError))));
  CHECK(connect(base_socket_.get(), SIGNAL(disconnected()),
                this, SLOT(OnDisconnected())));

  ssl_negotiation_timer_ = new QTimer(this);
  CHECK(connect(ssl_negotiation_timer_, SIGNAL(timeout()),
                this, SLOT(SslTimeout())));
  ssl_negotiation_timer_->setSingleShot(true);
  ssl_negotiation_timer_->start(kTimeout);

  base_socket_->startClientEncryption();
}

void SmtpProbe::SslTimeout() {
  LogDetails("SSL", true, "Negotiation timed out");
  ProbeDone(SMTP_SERVER_CERT_CANNOTVALIDATE);
  Cleanup();
}

void SmtpProbe::OnSslError(const QList<QSslError> &errors) {
  ssl_negotiation_timer_->stop();
  // TODO: Unify this code with the one in popprobe.cc
  QString debug = base_socket_->peerCertificate().toPem();
  switch (errors[0].error()) {
    case QSslError::NoError:
      break;
    case QSslError::HostNameMismatch:
      LogDetails("SSL", false, "Hostname on cert does not match: " + debug);
      ProbeDone(SMTP_SERVER_CERT_NAME_MISMATCH);
      break;
    case QSslError::SelfSignedCertificate:
    case QSslError::SelfSignedCertificateInChain:
      LogDetails("NEGOTIATETLS", false,
                 "Found self-signed cert in cert chain: " + debug);
      ProbeDone(SMTP_SERVER_CERT_SELFSIGNED);
      return;
    case QSslError::CertificateExpired:
      LogDetails("NEGOTIATETLS", false,
                 "Found expired certificate in cert chain: " + debug);
      ProbeDone(SMTP_SERVER_CERT_EXPIRED);
      return;
    case QSslError::CertificateNotYetValid:
      LogDetails("NEGOTIATETLS", false,
                 "Found not yet valid certificate in cert chain: " + debug);
      ProbeDone(SMTP_SERVER_CERT_NOTYETVALID);
      return;
    case QSslError::UnableToVerifyFirstCertificate:
    default:
      LogDetails("NEGOTIATETLS", false,
                 "Unable to validate server certificate: " + debug);
      ProbeDone(SMTP_SERVER_CERT_CANNOTVALIDATE);
      return;
  }
}

void SmtpProbe::OnSslEstablished() {
  ssl_negotiation_timer_->stop();
  src_ = base_socket_.get();
  bool expect_banner = smtp_port_ == SMTP_MSA_SSL_PORT;
  if (expect_banner) {
    StartProbeBanner();
  } else {
    // now that we're secure, re-issue EHLO.
    StartProbeEhlo();
  }
}

void SmtpProbe::StartProbeAuth() {
  SetState(AUTH);
  // TODO: Test long password.  Make sure it isn't line wrapped.
  //       Some Base64 implementations introduce line breaks.
  QString payload = username_ + '\0' + username_ + '\0' + password_;
  QByteArray encoded = payload.toLatin1().toBase64();
  QString encoded_str = QString::fromLatin1(encoded.data(), encoded.length());
  QString req = "AUTH PLAIN " + encoded_str;
  GetReader()->SendCommand(req);
}

void SmtpProbe::ProcessAuthResponse(SmtpResponseReader *reader) {
  int code = reader->response_code();
  switch (code) {
    case 235:
      // login succeeded.
      LogDetails("AUTH", true, reader->describe());
      break;
    case 454:
      LogDetails("AUTH", false, "Backend offline: " + reader->describe());
      ProbeDone(SMTP_SERVER_LOGIN_BACKEND_OFFLINE);
      break;
    case 535:
      // bad credentials
      LogDetails("AUTH", false, "Bad Credentials: " + reader->describe());
      ProbeDone(SMTP_SERVER_LOGIN_REJECTED);
      return;
    default:
      LogDetails("AUTH", false, "Unexpected code: " + reader->describe());
      ProbeDone(SMTP_SERVER_LOGIN_BAD_CODE);
      return;
  }
  StartProbeMailFrom();
}

void SmtpProbe::StartProbeMailFrom() {
  SetState(MAILFROM);
  QString cmd = "MAIL FROM: <";
  cmd += username_;
  cmd += ">";
  GetReader()->SendCommand(cmd);
}

void SmtpProbe::ProcessMailFromResponse(SmtpResponseReader *reader) {
  if (reader->response_code() == SMTP_GENERIC_OK) {
    LogDetails("MAIL FROM", true, reader->describe());
    StartProbeRcptTo();
  } else {
    LogDetails("MAIL FROM", false, "Rejected: " + reader->describe());
    ProbeDone(SMTP_SERVER_REJECTED_MAIL_FROM);
    return;
  }
}

void SmtpProbe::StartProbeRcptTo() {
  SetState(RCPTTO);
  GetReader()->SendCommand("RCPT TO: <" + username_ + ">");
}

void SmtpProbe::ProcessRcptToResponse(SmtpResponseReader *reader) {
  if (reader->response_code() != SMTP_GENERIC_OK) {
    LogDetails("RCPT TO", false, "Rejected: " + reader->describe());
    ProbeDone(SMTP_SERVER_REJECTED_RCPT_TO);
    return;
  }
  LogDetails("RCPT TO", true, reader->describe());
  // bypass the data command, go straight to rset/quit/drop:
  StartProbeRset();
}

void SmtpProbe::StartProbeRset() {
  SetState(RSET);
  GetReader()->SendCommand("RSET");
}

void SmtpProbe::ProcessRsetResponse(SmtpResponseReader *reader) {
  int code = reader->response_code();
  if (code != SMTP_GENERIC_OK) {
    LogDetails("RSET", false, "Rejected: " + reader->describe());
    ProbeDone(SMTP_SERVER_FAILED_RSET);
    return;
  }
  LogDetails("RSET", true, reader->describe());
  StartProbeQuit();
}

void SmtpProbe::StartProbeQuit() {
  SetState(QUIT);
  GetReader()->SendCommand("QUIT");
}

void SmtpProbe::ProcessQuitResponse(SmtpResponseReader *reader) {
  int code = reader->response_code();
  if (code != SMTP_QUIT_OK) {
    LogDetails("QUIT", false, "Rejected: " + reader->describe());
    ProbeDone(SMTP_SERVER_REJECTED_QUIT);
    return;
  }
  LogDetails("QUIT", true, reader->describe());
  // TODO: Need to process multiple connections.
  //       Only ProbeDone(OK) on the last one.
  ProbeDone(OK);
}

void SmtpProbe::OnError(QAbstractSocket::SocketError err) {
  CHECK(state_ != CONNECT);
  QString error_string = base_socket_->errorString();
  if (base_socket_ != NULL) {
    error_string = base_socket_->errorString();
    base_socket_->close();
  }
  if (we_closed_ || fault_ != OK) {
    return;
  }
  LogDetails("SocketError", false,
             QString("Error %1: %2").arg(err).arg(error_string));
}


SmtpResponseReader::SmtpResponseReader(QObject *parent,
                                       RenamableSslSocket *src,
                                       int timeout_in_millis)
  : QObject(parent), socket_(src), response_lines_(), extended_codes_(),
    response_type_(SMTP_RESPONSE_MALFORMED),
    anything_read_(false), resp_code_(0), reader_(src, timeout_in_millis) {
  // TODO: These objects are leaking.  Fix that.
  CHECK(connect(&reader_, SIGNAL(DataReady(LineReadResult)),
        this, SLOT(DataAvailable(LineReadResult))));
}

SmtpResponseReader::~SmtpResponseReader() {
}

void SmtpResponseReader::DataAvailable(LineReadResult result) {
  switch (result) {
    case DATA_READ:
      TryRead();
      break;
    case TIMEOUT:
      Done(SMTP_RESPONSE_TIMEOUT);
      break;
    case SOCKET_DROPPED:
      Done(SMTP_SOCKET_DROPPED);
      break;
  }
}

void SmtpResponseReader::SendCommand(const QString &cmd) {
  if (!cmd.isEmpty()) {
    Probe::Debug("SMTP sending line: %s\n", qPrintable(cmd));
    QString to_write(cmd + "\r\n");
    socket_->write(qPrintable(to_write));
    socket_->flush();
  }
}

void SmtpResponseReader::Done(SmtpResponseType result) {
  response_type_ = result;
  reader_.Cleanup();
  emit ResponseRead(this);
}

void SmtpResponseReader::TryRead() {
  QString line;
  while (reader_.NonblockingRead(&line)) {
    ProcessLine(line);
  }
}

void SmtpResponseReader::ProcessLine(const QString &line) {
  anything_read_ = true;
  Probe::Debug("SMTP received line: %s", qPrintable(line));
  QRegExp pattern(
    "^(\\d{3})([ \\-])(?:\\[([^\\]]+)\\])?([^\r\n]*)(?:\r\n)?$");
  // indexIn returns match position.  Should match at position 0
  int match_loc = pattern.indexIn(line);
  if (0 != match_loc) {
    response_lines_.push_back(line);
    Done(SMTP_RESPONSE_MALFORMED);
    return;
  }
  resp_code_ = pattern.cap(1).toInt();
  // lines that start with code then a dash indicate more responses to come.
  bool continuation_expected = pattern.cap(2) == "-";
  QString extended_code = pattern.cap(3);
  if (extended_code.size() != 0) {
    extended_codes_.insert(extended_code);
  }
  QString remainder = pattern.cap(4);
  response_lines_.append(remainder);
  if (!continuation_expected) {
    Done(SMTP_RESPONSE_COMPLETE);
  } else {
    // more lines expected
  }
}

QString SmtpResponseReader::describe() {
  QString rval;
  QTextStream stream(&rval, QIODevice::WriteOnly);
  if (!anything_read_) {
    stream << "No response was read.\n";
    return rval;
  } else {
    switch (response_type_) {
      case SMTP_RESPONSE_COMPLETE:
        break;
      case SMTP_RESPONSE_MALFORMED:
        stream << "Response was malformed: ";
        break;  // dump the rest
      case SMTP_SOCKET_DROPPED:
        stream << "ConnDrop: ";
        break;
      case SMTP_RESPONSE_TIMEOUT:
        stream << "Timeout: ";
        break;
    }
  }
  stream << "<code>" << resp_code_;
  if (extended_codes_.size() != 0) {
    stream << " [";
    for (QSet<QString>::const_iterator i = extended_codes_.begin();
          i != extended_codes_.end(); ++i) {
      stream << " ";
      stream << *i;
    }
    stream << "]";
  }
  stream << " ";
  for (int i = 0; i < response_lines_.size(); ++i) {
    if (i > 0) {
      stream << "\n\t";
    }
    QString foo;
    stream << response_lines_[i].trimmed();
  }
  stream << "</code>";
  return rval;
}

}  // namespace popdiag
